﻿/***

    Windows Form基类，用于显示无边框窗体的动画效果

    Austin Liu 刘恒辉
    Project Manager and Software Designer

    E-Mail: lzhdim@163.com
    Blog:   http://lzhdim.cnblogs.com
    Date:   2024-01-15 15:18:00

    说明：
        作为应用程序窗体的基类，提供了基本的操作及动画效果；

    使用方法：
        1、先将窗体的FormBoderStyle改成：
            1）FixedSingle； 2）Fixed3D； 3）FixedDialog；4）Sizeable
        2、更改窗体的Form继承基类为FormBase；
        3、更改窗体的ControlBox为True；
        4、在窗体中添加一个Panel，改Dock为Fill，用来作为窗体的边框；
        5、在Panel里添加其它窗体控件，用以实现窗体的功能；
        6、在窗体里添加窗体最小化、窗体最大化的按钮，用下面的代码来实现；
        7、操作窗体其它控件功能；
        8、通过窗体移动函数来移动窗体；

    注意：
        如果想要有移动无边框窗体到屏幕顶部显示最大化那个效果，需要设置FormBoderStyle = Sizeable，且MaximizeBox = true；

***/

namespace Lzhdim.Animation.UI
{
    using System;
    using System.Drawing;
    using System.Runtime.InteropServices;
    using System.Windows.Forms;

    /// <summary>
    /// Windows Form基类
    /// </summary>
    public class FormBase : Form
    {
        #region 使用到的Windows API

        private enum WindowMessages
        {
            WM_NULL = 0x0000,
            WM_CREATE = 0x0001,
            WM_DESTROY = 0x0002,
            WM_MOVE = 0x0003,
            WM_SIZE = 0x0005,
            WM_ACTIVATE = 0x0006,
            WM_SETFOCUS = 0x0007,
            WM_KILLFOCUS = 0x0008,
            WM_ENABLE = 0x000A,
            WM_SETREDRAW = 0x000B,
            WM_SETTEXT = 0x000C,
            WM_GETTEXT = 0x000D,
            WM_GETTEXTLENGTH = 0x000E,
            WM_PAINT = 0x000F,
            WM_CLOSE = 0x0010,

            WM_QUIT = 0x0012,
            WM_ERASEBKGND = 0x0014,
            WM_SYSCOLORCHANGE = 0x0015,
            WM_SHOWWINDOW = 0x0018,

            WM_ACTIVATEAPP = 0x001C,

            WM_SETCURSOR = 0x0020,
            WM_MOUSEACTIVATE = 0x0021,
            WM_GETMINMAXINFO = 0x24,
            WM_WINDOWPOSCHANGING = 0x0046,
            WM_WINDOWPOSCHANGED = 0x0047,

            WM_CONTEXTMENU = 0x007B,
            WM_STYLECHANGING = 0x007C,
            WM_STYLECHANGED = 0x007D,
            WM_DISPLAYCHANGE = 0x007E,
            WM_GETICON = 0x007F,
            WM_SETICON = 0x0080,

            // non client area
            WM_NCCREATE = 0x0081,

            WM_NCDESTROY = 0x0082,
            WM_NCCALCSIZE = 0x0083,
            WM_NCHITTEST = 0x84,
            WM_NCPAINT = 0x0085,
            WM_NCACTIVATE = 0x0086,

            WM_GETDLGCODE = 0x0087,

            WM_SYNCPAINT = 0x0088,

            // non client mouse
            WM_NCMOUSEMOVE = 0x00A0,

            WM_NCLBUTTONDOWN = 0x00A1,
            WM_NCLBUTTONUP = 0x00A2,
            WM_NCLBUTTONDBLCLK = 0x00A3,
            WM_NCRBUTTONDOWN = 0x00A4,
            WM_NCRBUTTONUP = 0x00A5,
            WM_NCRBUTTONDBLCLK = 0x00A6,
            WM_NCMBUTTONDOWN = 0x00A7,
            WM_NCMBUTTONUP = 0x00A8,
            WM_NCMBUTTONDBLCLK = 0x00A9,

            // keyboard
            WM_KEYDOWN = 0x0100,

            WM_KEYUP = 0x0101,
            WM_CHAR = 0x0102,

            WM_SYSCOMMAND = 0x0112,

            // menu
            WM_INITMENU = 0x0116,

            WM_INITMENUPOPUP = 0x0117,
            WM_MENUSELECT = 0x011F,
            WM_MENUCHAR = 0x0120,
            WM_ENTERIDLE = 0x0121,
            WM_MENURBUTTONUP = 0x0122,
            WM_MENUDRAG = 0x0123,
            WM_MENUGETOBJECT = 0x0124,
            WM_UNINITMENUPOPUP = 0x0125,
            WM_MENUCOMMAND = 0x0126,

            WM_CHANGEUISTATE = 0x0127,
            WM_UPDATEUISTATE = 0x0128,
            WM_QUERYUISTATE = 0x0129,

            // mouse
            WM_MOUSEFIRST = 0x0200,

            WM_MOUSEMOVE = 0x0200,
            WM_LBUTTONDOWN = 0x0201,
            WM_LBUTTONUP = 0x0202,
            WM_LBUTTONDBLCLK = 0x0203,
            WM_RBUTTONDOWN = 0x0204,
            WM_RBUTTONUP = 0x0205,
            WM_RBUTTONDBLCLK = 0x0206,
            WM_MBUTTONDOWN = 0x0207,
            WM_MBUTTONUP = 0x0208,
            WM_MBUTTONDBLCLK = 0x0209,
            WM_MOUSEWHEEL = 0x020A,
            WM_MOUSELAST = 0x020D,

            WM_PARENTNOTIFY = 0x0210,
            WM_ENTERMENULOOP = 0x0211,
            WM_EXITMENULOOP = 0x0212,

            WM_NEXTMENU = 0x0213,
            WM_SIZING = 0x0214,
            WM_CAPTURECHANGED = 0x0215,
            WM_MOVING = 0x0216,

            WM_ENTERSIZEMOVE = 0x0231,
            WM_EXITSIZEMOVE = 0x0232,

            WM_MOUSELEAVE = 0x02A3,
            WM_MOUSEHOVER = 0x02A1,
            WM_NCMOUSEHOVER = 0x02A0,
            WM_NCMOUSELEAVE = 0x02A2,

            WM_MDIACTIVATE = 0x0222,
            WM_HSCROLL = 0x0114,
            WM_VSCROLL = 0x0115,

            WM_SYSMENU = 0x313,

            WM_PRINT = 0x0317,
            WM_PRINTCLIENT = 0x0318,
        }

        [Flags]
        private enum WindowStyle
        {
            WS_OVERLAPPED = 0x00000000,
            WS_POPUP = -2147483648, //0x80000000,
            WS_CHILD = 0x40000000,
            WS_MINIMIZE = 0x20000000,
            WS_VISIBLE = 0x10000000,
            WS_DISABLED = 0x08000000,
            WS_CLIPSIBLINGS = 0x04000000,
            WS_CLIPCHILDREN = 0x02000000,
            WS_MAXIMIZE = 0x01000000,
            WS_CAPTION = 0x00C00000,
            WS_BORDER = 0x00800000,
            WS_DLGFRAME = 0x00400000,
            WS_VSCROLL = 0x00200000,
            WS_HSCROLL = 0x00100000,
            WS_SYSMENU = 0x00080000,
            WS_THICKFRAME = 0x00040000,
            WS_GROUP = 0x00020000,
            WS_TABSTOP = 0x00010000,
            WS_MINIMIZEBOX = 0x00020000,
            WS_MAXIMIZEBOX = 0x00010000,
            WS_TILED = WS_OVERLAPPED,
            WS_ICONIC = WS_MINIMIZE,
            WS_SIZEBOX = WS_THICKFRAME,
            WS_TILEDWINDOW = WS_OVERLAPPEDWINDOW,

            WS_OVERLAPPEDWINDOW = (WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU |
                                    WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX),

            WS_POPUPWINDOW = (WS_POPUP | WS_BORDER | WS_SYSMENU),
            WS_CHILDWINDOW = (WS_CHILD)
        }

        [DllImport("gdi32.dll")]
        private static extern IntPtr CreateRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);

        [DllImport("gdi32.dll")]
        private static extern bool DeleteObject(IntPtr hObj);

        [DllImport("gdi32.dll")]
        private static extern int GetRgnBox(IntPtr hrgn, out RECT lprc);

        [DllImport("user32.dll")]
        private static extern int GetSystemMetrics(int smIndex);

        [DllImport("user32.dll")]
        private static extern Int32 GetWindowLong(IntPtr hWnd, Int32 Offset);

        [DllImport("user32.dll")]
        private static extern int GetWindowRgn(IntPtr hWnd, IntPtr hRgn);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool IsWindowVisible(IntPtr hWnd);

        [DllImport("user32.dll")]
        private static extern int SendMessage(IntPtr hwnd, int msg, int wparam, int lparam);

        [DllImport("user32.dll")]
        private static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);

        [DllImport("shell32.dll")]
        private static extern int SHAppBarMessage(uint dwMessage, [In] ref APPBARDATA pData);

        [StructLayout(LayoutKind.Sequential)]
        private struct APPBARDATA
        {
            public int cbSize; // initialize this field using: Marshal.SizeOf(typeof(APPBARDATA));
            public IntPtr hWnd;
            public uint uCallbackMessage;
            public uint uEdge;
            public RECT rc;
            public int lParam;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct RECT
        {
            public int left;
            public int top;
            public int right;
            public int bottom;

            public RECT(int left, int top, int right, int bottom)
            {
                this.left = left;
                this.top = top;
                this.right = right;
                this.bottom = bottom;
            }

            public static RECT FromXYWH(int x, int y, int width, int height)
            {
                return new RECT(x,
                                y,
                                x + width,
                                y + height);
            }
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct WINDOWPOS
        {
            internal IntPtr hwnd;
            internal IntPtr hWndInsertAfter;
            internal int x;
            internal int y;
            internal int cx;
            internal int cy;
            internal uint flags;
        }

        private static class NativeConstants
        {
            public const uint ABM_GETSTATE = 0x4;
            public const int ABS_AUTOHIDE = 0x1;
            public const int GWL_EXSTYLE = (-20);
            public const int GWL_ID = (-12);
            public const int GWL_STYLE = (-16);
            public const int SM_CXPADDEDBORDER = 92;
            public const int SM_CXSIZEFRAME = 32;
            public const int SM_CYSIZEFRAME = 33;
            public const uint TPM_LEFTBUTTON = 0x0000;
            public const uint TPM_RETURNCMD = 0x0100;
            public const uint TPM_RIGHTBUTTON = 0x0002;
            public const int WM_NCLBUTTONDOWN = 0x00A1;
            public const int WM_NCRBUTTONUP = 0x00A5;
            public static readonly IntPtr FALSE = new IntPtr(0);
            public static readonly IntPtr TRUE = new IntPtr(1);
        }

        #endregion 使用到的Windows API

        #region 属性

        /// <summary>
        /// 窗体的显示状态
        /// </summary>
        private FormWindowState MinMaxState
        {
            get
            {
                var s = GetWindowLong(Handle, NativeConstants.GWL_STYLE);
                var max = (s & (int)WindowStyle.WS_MAXIMIZE) > 0;
                if (max) return FormWindowState.Maximized;
                var min = (s & (int)WindowStyle.WS_MINIMIZE) > 0;
                if (min) return FormWindowState.Minimized;
                return FormWindowState.Normal;
            }
        }

        #endregion 属性

        #region 使用到的内部函数

        private void SetWindowRegion(IntPtr hwnd, int left, int top, int right, int bottom)
        {
            var rgn = CreateRectRgn(0, 0, 0, 0);
            var hrg = new HandleRef((object)this, rgn);
            var r = GetWindowRgn(hwnd, hrg.Handle);
            RECT box;
            GetRgnBox(hrg.Handle, out box);
            if (box.left != left || box.top != top || box.right != right || box.bottom != bottom)
            {
                var hr = new HandleRef((object)this, CreateRectRgn(left, top, right, bottom));
                SetWindowRgn(hwnd, hr.Handle, IsWindowVisible(hwnd));
            }
            DeleteObject(rgn);
        }

        private void WmNCActivate(ref Message msg)
        {
            // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/windows/windowreference/windowmessages/wm_ncactivate.asp

            bool active = (msg.WParam == NativeConstants.TRUE);

            if (MinMaxState == FormWindowState.Minimized)
                DefWndProc(ref msg);
            else
            {
                // repaint title bar
                //PaintNonClientArea(msg.HWnd, (IntPtr)1);

                // allow to deactivate window
                msg.Result = NativeConstants.TRUE;
            }
        }

        private void WmNCCalcSize(ref Message m)
        {
            // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/windows/windowreference/windowmessages/wm_nccalcsize.asp
            // http://groups.google.pl/groups?selm=OnRNaGfDEHA.1600%40tk2msftngp13.phx.gbl

            var r = (RECT)Marshal.PtrToStructure(m.LParam, typeof(RECT));
            var max = MinMaxState == FormWindowState.Maximized;

            if (max)
            {
                var x = GetSystemMetrics(NativeConstants.SM_CXSIZEFRAME);
                var y = GetSystemMetrics(NativeConstants.SM_CYSIZEFRAME);
                var p = GetSystemMetrics(NativeConstants.SM_CXPADDEDBORDER);
                var w = x + p;
                var h = y + p;

                r.left += w;
                r.top += h;
                r.right -= w;
                r.bottom -= h;

                var appBarData = new APPBARDATA();
                appBarData.cbSize = Marshal.SizeOf(typeof(APPBARDATA));
                var autohide = (SHAppBarMessage(NativeConstants.ABM_GETSTATE, ref appBarData) & NativeConstants.ABS_AUTOHIDE) != 0;
                if (autohide) r.bottom -= 1;

                Marshal.StructureToPtr(r, m.LParam, true);
            }

            m.Result = IntPtr.Zero;
        }

        private void WmNCPaint(ref Message msg)
        {
            // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/pantdraw_8gdw.asp
            // example in q. 2.9 on http://www.syncfusion.com/FAQ/WindowsForms/FAQ_c41c.aspx#q1026q

            // The WParam contains handle to clipRegion or 1 if entire window should be repainted
            //PaintNonClientArea(msg.HWnd, (IntPtr)msg.WParam);

            // we handled everything
            msg.Result = NativeConstants.TRUE;
        }

        private void WmSetText(ref Message msg)
        {
            // allow the system to receive the new window title
            DefWndProc(ref msg);

            // repaint title bar
            //PaintNonClientArea(msg.HWnd, (IntPtr)1);
        }

        private void WmWindowPosChanged(ref Message m)
        {
            DefWndProc(ref m);
            UpdateBounds();
            var pos = (WINDOWPOS)Marshal.PtrToStructure(m.LParam, typeof(WINDOWPOS));

            //2024年2月4日添加下列判断代码，因为窗体TopMost的时候底部会长出来；
            if (MinMaxState == FormWindowState.Maximized)
            {
                //如果是最大化，需要去掉底部的长出部分
                SetWindowRegion(m.HWnd, 0, 0, pos.cx, pos.cy - 8);
            }
            else
            {
                SetWindowRegion(m.HWnd, 0, 0, pos.cx, pos.cy);
            }

            m.Result = NativeConstants.TRUE;
        }

        #endregion 使用到的内部函数

        #region 显示窗体默认菜单

        /// <summary>
        /// 转换短整型
        /// </summary>
        /// <param name="lowPart">低位</param>
        /// <param name="highPart">高位</param>
        /// <returns></returns>
        protected static int MakeLong(short lowPart, short highPart)
        {
            return (int)(((ushort)lowPart) | (uint)(highPart << 16));
        }

        /// <summary>
        /// 显示系统默认菜单
        /// </summary>
        /// <param name="buttons">鼠标按键</param>
        protected void ShowSystemMenu(MouseButtons buttons)
        {
            ShowSystemMenu(buttons, MousePosition);
        }

        /// <summary>
        /// 显示系统默认菜单
        /// </summary>
        /// <param name="buttons">鼠标按键</param>
        /// <param name="pos">坐标</param>
        protected void ShowSystemMenu(MouseButtons buttons, Point pos)
        {
            SendMessage(Handle, (int)WindowMessages.WM_SYSMENU, 0, MakeLong((short)pos.X, (short)pos.Y));
        }

        #endregion 显示窗体默认菜单

        #region 重写的函数

        /// <summary>
        /// 事件处理创建时事件
        /// </summary>
        /// <param name="e">事件参数</param>
        protected override void OnHandleCreated(EventArgs e)
        {
            base.OnHandleCreated(e);

            if (!DesignMode)
                SetWindowRegion(Handle, 0, 0, Width, Height);
        }

        /// <summary>
        /// 接收的消息处理函数
        /// </summary>
        /// <param name="m">消息代码</param>
        protected override void WndProc(ref Message m)
        {
            if (DesignMode)
            {
                base.WndProc(ref m);
                return;
            }

            switch (m.Msg)
            {
                case (int)WindowMessages.WM_NCCALCSIZE:
                    {
                        // Provides new coordinates for the window client area.
                        WmNCCalcSize(ref m);
                        break;
                    }
                case (int)WindowMessages.WM_NCPAINT:
                    {
                        // Here should all our painting occur, but...
                        WmNCPaint(ref m);
                        break;
                    }
                case (int)WindowMessages.WM_NCACTIVATE:
                    {
                        // ... WM_NCACTIVATE does some painting directly
                        // without bothering with WM_NCPAINT ...
                        WmNCActivate(ref m);
                        break;
                    }
                case (int)WindowMessages.WM_SETTEXT:
                    {
                        // ... and some painting is required in here as well
                        WmSetText(ref m);
                        break;
                    }
                case (int)WindowMessages.WM_WINDOWPOSCHANGED:
                    {
                        WmWindowPosChanged(ref m);
                        break;
                    }
                case 174: // ignore magic message number
                    {
                        break;
                    }
                default:
                    {
                        base.WndProc(ref m);
                        break;
                    }
            }
        }

        #endregion 重写的函数

        #region 窗体移动函数

        private const int HTCAPTION = 2;

        private const int VM_NCLBUTTONDOWN = 0XA1;

        /// <summary>
        /// 鼠标点下事件
        /// </summary>
        /// <param name="form">窗体</param>
        /// <param name="p">事件中的点位置</param>
        public static void DecorationMouseDown(Form form, Point p)
        {
            ReleaseCapture();
            var pt = new POINTS { X = (short)p.X, Y = (short)p.Y };
            SendMessage(form.Handle, (int)0x00A1, (int)2, pt);
        }

        /// <summary>
        /// 鼠标点起事件
        /// </summary>
        /// <param name="form">窗体</param>
        /// <param name="p">事件中的点位置</param>
        public static void DecorationMouseUp(Form form, Point p)
        {
            ReleaseCapture();
            var pt = new POINTS { X = (short)p.X, Y = (short)p.Y };
            SendMessage(form.Handle, (int)0x00A2, (int)2, pt);
        }

        //定义鼠标左键按下
        /// <summary>
        /// 模拟鼠标移动窗体标题栏，用于主窗体能够有移动到屏幕顶部最大化效果
        ///
        /// 注意：
        ///     如果想要有移动无边框窗体到屏幕顶部显示最大化那个效果，需要设置FormBoderStyle = Sizeable，且MaximizeBox = true；
        /// </summary>
        /// <param name="form">窗体</param>
        public static void MouseDownEffect(Form form)
        {
            ReleaseCapture();
            SendMessage(form.Handle, VM_NCLBUTTONDOWN, HTCAPTION, 0);
        }

        [DllImport("user32.dll")]
        private static extern bool ReleaseCapture();

        [DllImport("user32.dll")]
        private static extern int SendMessage(IntPtr hwnd, int msg, int wparam, POINTS pos);

        [StructLayout(LayoutKind.Sequential)]
        private struct POINTS
        {
            public short X;
            public short Y;
        }

        #endregion 窗体移动函数
    }
}